Repository navigation
Engagement scope guard: every network target read and checked (DNS, scanners, nc) - #18
Conversation
Phase 10, step 5 plan, PR 4. The engagement scope check belongs to the
security layer, but steps are judged in the core, which never imports
that layer. The core now keeps two hook lists:
- add_step_check(fn): fn(step) returns why the step is refused, or
None;
- add_step_notice(fn): fn(step) returns why it must be confirmed, or
None.
SafetyEngine asks the checks after its own rules (an allow-list refusal
is reported as such first), for every step and again right before it
runs (may_execute), and the notices in confirmation_reason. A hook
that raises refuses the step: a broken check must never let one
through. The engine does not know what the hooks are -- complete
mediation without a dependency on the layer. Manual (Safety and
Sandbox > The Safety Engine) and the test list updated.
Phase 10, step 5 plan, PR 4 (decision D14i). Until now no code read a
network step's target: the engagement's scope was only recorded.
codebud_sec/scope.py reads every target a network tool will contact
from its command line and checks it against the engagement:
- active tools (nmap, masscan, nc, ncat) are refused when a target is
outside the scope, excluded, or the window is closed -- a hard
block: a model's plan is untrusted, and the UK AISI incident report
(4 Aug 2026) shows prompt-level scope is not enough;
- passive lookups (dig, host) out of scope ask for confirmation,
D14's warn-and-confirm kept for them.
A target that cannot be read is refused. Each tool's parser knows which
options take values; an unknown option is refused, since its value
could be a target the guard cannot see. Hosts a tool contacts besides
its target are targets too: DNS servers (dig @, nmap --dns-servers),
proxies (nc -x, ncat --proxy, nmap --proxies), the idle-scan zombie
(nmap -sI) and the FTP relay (nmap -b). nmap -iR (random hosts),
--resume, masscan -c and dig -f are refused; target lists (-iL) are
read only from a file inside the project. Ranges -- CIDRs, masscan
a-b, nmap octet ranges by their bounding range -- must fit in the scope
as a whole, and engagement.covers_network() checks that against the
excluded networks too. A name never matches addresses: resolving it
would itself be a network action.
A step that declares the network with a tool the guard cannot read is
refused, and an engagement at privacy trusted (no isolation) refuses
every command. ScopeGuard.activate() registers the guard with the
core's step-check hook; the codebud-sec entry point will open the
engagement and activate it (PR 7). CORE_API widens by codebud.safety
(the hook) and codebud.privacy, deliberately.
HTTP tools, web fuzzers and TLS scanners follow in PR 4b. Architecture
layout, build list and test list updated.
Review summary: changes requestedOne full sweep over Conditions.
Correct, verified (63 tests):
Defects (reproduced on
Verdict: changes requested. The fixes come as new commits, then a re-review. |
Review of PR #18, findings 1 and 2. The guard read targets from every word naming a tool, in every command step, so data was taken for a command: `grep host /etc/hosts` and `grep -c nmap auth.log` were refused, "host" and "nmap" read as the tools. A step that does not declare the network contacts no one when it is isolated, so only network steps are checked now. That relies on isolation, which the guard did not require: at privacy private with no isolation tool, codebud warns and runs steps unisolated, and an undeclared step then has the network. An engagement now refuses every command step unless isolation is available (and the privacy level is not trusted). CORE_API widens by codebud.isolation, deliberately. The tests now decide isolation for themselves instead of inheriting the machine's (macOS CI has no bubblewrap); the suite was also run with isolation forced unavailable.
Review of PR #18, finding 3. nmap's scripts can contact hosts the command line never names, and the guard let them through: - broadcast-* scripts send to the whole local network; they are in the broadcast and discovery categories and partly in safe, so --script safe or --script discovery runs them too; - targets-* scripts with --script-args newtargets add the hosts they find as scan targets; - external scripts send data to third-party services (whois, ASN). --script is refused when any name, category or glob in it could select these -- broadcast, discovery, safe, external, all, broadcast*, targets-*, or a glob matching them -- in a list or a boolean expression (it errs strict: "not broadcast" is refused too, since expressions are not evaluated); --script-args with newtargets is refused. -sC and named scripts such as http-title stay allowed. Eleven cases, all allowed before.
Re-review of PR #18. The guard judged only command steps, but web_search is a network step too: it sends the query to DuckDuckGo, a third party outside any engagement. Web search is off by default and the security profiles keep it off, but the guard should not depend on a setting; it now refuses web_search steps. File tools and host_network stay on this machine and are untouched (tested).
Re-review summary: fixed, ready to merge once CI passesSecond sweep, over
Erring strict, documented in the code: 1535 tests pass, ruff is clean, and |
|
Merging. All four review findings are fixed and re-verified: data words, isolation, nmap scripts, web_search. CI is green on all five jobs at Next, from the step 5 plan: PR 4b, target parsers for the HTTP tools, web fuzzers and TLS scanners. |
Found on macOS CI (PR mkhellat#18's run, macOS Py 3.10): unlocking a key made a moment earlier failed with pyrage's "Excessive work parameter for passphrase", which codebud reported as "wrong passphrase". age sets the passphrase's scrypt work factor from the machine's speed when it seals the key, and on every unlock measures the speed again and refuses a work factor far above it. On a busy or throttled moment -- a loaded runner, a laptop on battery -- a correct passphrase is refused, and codebud told the user it was wrong: a lockout that sends them looking for a typo, or giving their data up for lost. pyrage offers no control over the work factor, so codebud now retries that specific refusal (age measures again each time) three times, then says what happened -- the machine was slower than when the key was sealed, the passphrase may well be right -- instead of "wrong passphrase". A genuinely wrong passphrase is still reported as such, and not retried. The manual's Troubleshooting chapter gains "Key Refused on a Slow Moment", including the persistent case of a key moved to a much slower machine.
PR 4 of the phase 10, step 5 plan (decision D14i): the engagement scope guard. Until now no code read a network step's target; the engagement's scope was only recorded in the evidence log.
Commits
3dcff8dfeat(safety): let an optional layer register step checks. The core keepsadd_step_check()andadd_step_notice()hook lists, whichSafetyEngineasks after its own rules, for every step and again right before it runs. A hook that raises refuses the step. The core never imports the security layer.35841cefeat(security): the engagement scope guard (DNS, scanners, nc).nmap,masscan,nc,ncat): refused out of scope, on excluded targets, or outside the window. A hard block, given the AISI 2026 evidence that prompt-level scope isn't enough.dig,host): ask for confirmation instead.nmap -iR,--resume,masscan -c,dig -f.trusted.CORE_APIwidens bycodebud.safetyandcodebud.privacy, deliberately.The guard isn't reachable by users yet: the
codebud-secentry point (PR 7) opens the engagement and activates it. HTTP tools, web fuzzers and TLS scanners follow in PR 4b.Testing
1516 tests pass (63 new). Ruff is clean, the manual builds without warnings, and
make distcheckpasses.